package com.andova.widget.timequantum

import android.content.Context
import android.graphics.*
import android.os.Build
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.widget.TextView
import android.widget.TimePicker
import androidx.annotation.RequiresApi
import com.andova.widget.R
import com.google.android.material.bottomsheet.BottomSheetDialog
import java.util.*
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min

/**
 * todo 限定每天最多能够创建的时间段？
 * todo 1.0 解决微调后实际显示可能有1的偏差
 * todo 1.0 时间段微调弹框首次加载速度问题
 * todo 2.0 控件组件化
 * todo 2.0 图标元素渲染
 * todo 2.0 拖拽实时时间醒目窗口展示
 * todo 2.0 时间刻度尺
 * todo 3.0 时间缩放与滑动功能 适配屏幕区域有限的场景
 * todo 3.0 水平方向操作模式
 * todo 4.0 动效添加
 */
class TimeQuantumView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    private val wPaint = Paint() // week        paint
    private val tPaint = Paint() // time        paint
    private val bPaint = Paint() // background  paint
    private val lPaint = Paint() // line        paint
    private val rPaint = Paint() // rect        paint
    private var tUnit = 0f
    private var mRegTop = 0f
    private var mMove = false
    private var mMerge = false
    private var mReadied = false
    private var mReadOnly = false
    private var mWeekDay = 0
    private var mMinMinutes = 0
    private var mWidthPixels = 0
    private var mGuideLineY = 0f
    private val mAbbrWeek: Array<String>
    private var mTimeQatm = newTQ()
    private var mTQs = mutableListOf<TimeQuantum>()
    private val mTQ_map = mutableMapOf<@Week Int, MutableList<TimeQuantum>>()
    private val mDayState = mutableMapOf<@Week Int, DayState?>()
    private var mListener: OnTimeQuantumListener? = null
    private var mDialog: BottomSheetDialog? = null

    private var tSize = 0f
    private var mWeekMargin = 0f
    private var mWeekPadding = 0f
    private var mTimePadding = 0f

    init {
        mWidthPixels = context.resources.displayMetrics.widthPixels
        mAbbrWeek = context.resources.getStringArray(R.array.array_week_abbr)
        val a = context.obtainStyledAttributes(attrs, R.styleable.TimeQuantumView)
        mWeekMargin = a.getDimensionPixelOffset(R.styleable.TimeQuantumView_tqv_weekDividerMargin, 4).toFloat()
        mWeekPadding = a.getDimensionPixelOffset(R.styleable.TimeQuantumView_tqv_weekVerticalPadding, 8).toFloat()
        mTimePadding = a.getDimensionPixelOffset(R.styleable.TimeQuantumView_tqv_timePadding, 16).toFloat()
        mMinMinutes = a.getInteger(R.styleable.TimeQuantumView_tqv_minMinutes, 15)
        bPaint.color = a.getColor(R.styleable.TimeQuantumView_tqv_weekBackgroundColor, R.attr.colorPrimary)
        wPaint.color = a.getColor(R.styleable.TimeQuantumView_tqv_weekTextColor, Color.WHITE)
        wPaint.textSize = a.getDimensionPixelOffset(R.styleable.TimeQuantumView_tqv_weekTextSize, 18).toFloat()
        tPaint.color = a.getColor(R.styleable.TimeQuantumView_tqv_timeTextColor, R.attr.titleTextColor)
        tSize = a.getDimensionPixelOffset(R.styleable.TimeQuantumView_tqv_timeTextSize, 18).toFloat()
        tPaint.textSize = tSize
        lPaint.color = bPaint.color
        swtPaint(rPaint, TQV_TYPE_PERIOD_DRAW)
        a.recycle()

        initPaint()
    }

    private fun initPaint() {
        bPaint.isAntiAlias = true
        lPaint.isAntiAlias = true
        tPaint.isAntiAlias = true
        wPaint.isAntiAlias = true
        wPaint.typeface = Typeface.DEFAULT_BOLD
        mRegTop = getRegTop()
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        val tHeight = height - mRegTop
        tUnit = 24 / tHeight
        if (mReadied) return
        mReadied = true
        mListener?.onReadied()
    }

    override fun onDraw(canvas: Canvas?) {
        drawForWeek(canvas)
        drawForTime(canvas)
        drawForPeriods(canvas)
        drawGuide(canvas)
    }

    private fun drawForWeek(canvas: Canvas?) {
        for (i in mAbbrWeek.indices) {
            val day = getWeekDay(i)
            var list = mTQ_map[day]
            if (list.isNullOrEmpty()) {
                list = mutableListOf()
                mTQ_map[day] = list
            }
            var ds = mDayState[day]
            if (ds == null) {
                ds = DayState(day, getWeekRect(i))
                mDayState[day] = ds
            }
            canvas?.drawRect(ds.rect, bPaint)
            val wd = mAbbrWeek[i]
            val w = ds.rect.right - ds.rect.left
            val x = ds.rect.left + w / 2 - getTextWidth(wPaint, wd) / 2
            canvas?.drawText(wd, x, getWeekY(ds.rect), wPaint)
        }
    }

    private fun drawForTime(canvas: Canvas?) {
        val tTop = mRegTop
        val tHeight = height - tTop
        val tUnit = tHeight / 24
        val h = getTextHeight(swtPaint(tPaint, TQV_TYPE_TIME_DARK))
        val da = tUnit > 2 * h
        val ch = getCurHour()
        val rs = getRegStart()
        val re = getRegEnd()
        for (i in 0..23) {
            val half = !da && i % 2 == 0
            val hour = i != ch
            val time = formatNum(i)
            when (i) {
                12, in 0..6, in 19..23 -> {
                    if (!half && hour) canvas?.drawText(time, mTimePadding, (tTop + h / 2 + tUnit / 2) + i * tUnit, swtPaint(tPaint, TQV_TYPE_TIME_DARK))
                    canvas?.drawRect(rs, tTop + i * tUnit, re, tTop + (i + 1) * tUnit, swtPaint(rPaint, TQV_TYPE_PERIOD_DARK))
                }
                else -> {
                    if (!half && hour) canvas?.drawText(time, mTimePadding, (tTop + h / 2 + tUnit / 2) + i * tUnit, swtPaint(tPaint, TQV_TYPE_TIME_LIGHT))
                    canvas?.drawRect(rs, tTop + i * tUnit, re, tTop + (i + 1) * tUnit, swtPaint(rPaint, TQV_TYPE_PERIOD_LIGHT))
                }
            }
        }
    }

    private fun drawForPeriods(canvas: Canvas?) {
        val mh = getTextHeight(wPaint) * 5
        swtPaint(lPaint, TQV_TYPE_LINE_THIN)
        swtPaint(rPaint, TQV_TYPE_PERIOD_DRAW)
        traversalTQ { rect, tq ->
            tq.time = null
            val l = rect.left
            val r = rect.right
            val s = tq.top
            val e = if (tq.bottom < height) tq.bottom else height - 0.1f
            s.apply {
                if (this <= 0) return@apply
                canvas?.drawLine(l, this, r, this, lPaint)
            }
            e.apply {
                if (this <= 0) return@apply
                canvas?.drawLine(l, this, r, this, lPaint)
            }
            if (s > 0 && e > s) {
                canvas?.drawRect(l, s, r, e, rPaint)
                val to = resources.getString(R.string.text_to)
                val time = getCurTime(s, e)
                tq.time = time
                if (e - s < mh) return@traversalTQ false
                canvas?.drawText(time.start, l + (r - l - getTextWidth(wPaint, time.start)) / 2, (e + s) / 2 - getTextHeight(wPaint) * 1.5f, wPaint)
                canvas?.drawText(to, l + (r - l - getTextWidth(wPaint, to)) / 2, (e + s) / 2, wPaint)
                canvas?.drawText(time.end, l + (r - l - getTextWidth(wPaint, time.end)) / 2, (e + s) / 2 + getTextHeight(wPaint) * 1.5f, wPaint)
            }
            false
        }
    }

    private fun drawGuide(canvas: Canvas?) {
        val b = mDayState[0]?.rect?.bottom ?: 0f
        if (mGuideLineY <= b) return
        val rect = mDayState[0]?.rect ?: return
        val l = rect.left - mTimePadding
        canvas?.drawLine(l, mGuideLineY, getRegEnd(), mGuideLineY, swtPaint(lPaint, TQV_TYPE_LINE_THICK))
        val h = getCurHour().toString()
        val m = getCurMinute().toString()
        val p = swtPaint(tPaint, TQV_TYPE_TIME_MINUTE)
        canvas?.drawText(h, mTimePadding, getWeekY(rect), p)
        canvas?.drawText(m, mTimePadding / 2, mGuideLineY + getTextHeight(p) / 4, p)
    }

    private fun getCurY(h: Int, m: Int): Float {
        val height = height - mRegTop
        val unit = height / 24f
        return mRegTop + h * unit + unit / 60 * m
    }

    private fun getCurHour(): Int {
        val r = (mGuideLineY - mRegTop) * tUnit
        return floor(r).toInt()
    }

    private fun getCurMinute(): Int {
        val r = (mGuideLineY - mRegTop) * tUnit
        val h = floor(r).toInt()
        return floor((r - h) * 60).toInt()
    }

    private fun getCurTime(sh: Int, sm: Int, eh: Int, em: Int): Time = Time(
        sh, sm, eh, em,
        "${formatNum(sh)}:${formatNum(sm)}",
        "${formatNum(eh)}:${formatNum(em)}"
    )

    private fun getCurTime(s: Float, e: Float): Time {
        val sr = (s - mRegTop) * tUnit
        val sh = floor(sr).toInt()
        val sm = floor((sr - sh) * 60).toInt()
        val er = (e - mRegTop) * tUnit
        val eh = floor(er).toInt()
        val em = floor((er - eh) * 60).toInt()
        return getCurTime(sh, sm, eh, em)
    }

    private fun formatNum(num: Int) = if (num < 10) "0$num" else num.toString()
    private fun getTextWidth(paint: Paint, content: String) = paint.measureText(content)
    private fun getTextHeight(paint: Paint) = paint.descent() - paint.ascent()
    private fun getRegTop() = getTextHeight(wPaint) + 2 * mWeekPadding
    private fun getRegEnd() = width - mTimePadding - mWeekMargin
    private fun getRegStart() = 2 * mTimePadding + getTextWidth(tPaint, "24")
    private fun getWeekY(rect: RectF): Float {
        val h = rect.bottom - rect.top
        return rect.top + h / 2 + (h - getTextHeight(wPaint)) / 2
    }

    @Week
    private fun getWeekDay(index: Int): Int {
        if (index < 0 || index > mAbbrWeek.size - 1) throw IllegalArgumentException("invalid index value $index what should be 0 to 6")
        return index
    }

    private fun getWeekRect(index: Int): RectF {
        val day = getWeekDay(index)
        val offset = getRegStart()
        val wUnit = (mWidthPixels - offset - mTimePadding) / mAbbrWeek.size
        val left = offset + wUnit * day
        return RectF(left, 0f, left + wUnit - mWeekMargin, mRegTop)
    }

    private fun mockForTouch(day: Int, sh: Int, sm: Int, eh: Int, em: Int) {
        val sy = getCurY(sh, sm)
        val ey = getCurY(eh, em)
        val tTop = mRegTop
        if (sy < tTop) return
        mTimeQatm.apply {
            top = sy
            bottom = ey
            time = getCurTime(sh, sm, eh, em)
            for (t in mTQs) {
                if (t == this) continue
                if (RectF.intersects(t, this)) {
                    top = min(t.top, top)
                    bottom = max(t.bottom, bottom)
                    val hs = if (top < t.top) time?.sh else t.time?.sh
                    val ms = if (top < t.top) time?.sm else t.time?.sm
                    val he = if (bottom > t.bottom) time?.eh else t.time?.eh
                    val me = if (bottom > t.bottom) time?.em else t.time?.em
                    mTQs.remove(t)
                    return mockForTouch(day, hs ?: 0, ms ?: 0, he ?: 0, me ?: 0)
                }
            }
        }
        val b = mTimeQatm.bottom.toInt()
        if (height > b && height - b < 2) mTimeQatm.bottom = height.toFloat()
        if (!removeIfNeeded(mTimeQatm)) mTimeQatm = newTQ()
        mTQ_map[day] = mTQs
        mListener?.onChanged(getResult())
        invalidate()
    }

    private fun touchForDown(event: MotionEvent) {
        dayOfTouch(event)
        if (mWeekDay == -1) return
        var y = event.y
        val tTop = mRegTop
        if (y < tTop) y = tTop
        for (qt in mTQs) if (y >= qt.top && y <= qt.bottom) {
            val half = (qt.top + qt.bottom) / 2
            if (y <= half) qt.top = y else qt.bottom = y
            mTimeQatm = qt
            invalidate()
            return
        }
        if (y < tTop) return
        mTimeQatm.top = y
        mTimeQatm.bottom = 0f
        mTQs.add(mTimeQatm)
        invalidate()
    }

    private fun dayOfTouch(event: MotionEvent) {
        mWeekDay = -1
        for (ds in mDayState.values) {
            val l = ds?.rect?.left ?: continue
            val r = ds.rect.right
            if (event.x < l || event.x > r) continue
            mWeekDay = ds.day
            mTQs = mTQ_map[mWeekDay] ?: mutableListOf()
        }
    }

    private fun touchForMove(event: MotionEvent): Boolean {
        if (mWeekDay == -1) return super.onTouchEvent(event)
        mTimeQatm.apply {
            mMove = true
            event.y.apply y@{
                if (this < mRegTop) {
                    top = mRegTop
                    return@y
                }
                val half = (top + bottom) / 2
                if (this <= half) {
                    if (mMerge) {
                        if (this < top) top = this
                    } else top = this
                } else {
                    if (mMerge) {
                        if (this > bottom) bottom = this
                    } else bottom = this
                }
            }
            if (mMerge) return@apply invalidate()
            mergeIfNeeded(this)?.apply {
                mTQs.remove(this)
                mMerge = true
            }
            invalidate()
        }
        return true
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        if (mReadOnly) return super.onTouchEvent(event)
        validTQ(mTimeQatm)
        mGuideLineY = event?.y ?: 0f
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> if (isRegCenter(event.x, event.y)) {
                dayOfTouch(event)
                if (mWeekDay == -1) return super.onTouchEvent(event)
                mGuideLineY = 0f
                bottomSheet(mWeekDay)
                return false
            } else touchForDown(event)
            MotionEvent.ACTION_MOVE -> return touchForMove(event)
            MotionEvent.ACTION_CANCEL -> {
                mMove = false
                mMerge = false
                mGuideLineY = 0f
                removeIfNeeded(mTimeQatm)
                invalidate()
                return super.onTouchEvent(event)
            }
            MotionEvent.ACTION_UP -> {
                mGuideLineY = 0f
                if (mWeekDay == -1) {
                    mMove = false
                    mMerge = false
                    return super.onTouchEvent(event)
                }
                mMerge = false
                mTQ_map[mWeekDay] = mTQs
                val y = event.y.toInt()
                if (height > y && height - y < 2) mTimeQatm.bottom = height.toFloat()
                if (!removeIfNeeded(mTimeQatm)) mTimeQatm = newTQ()
                invalidate()
                mListener?.onChanged(getResult())
                if (y >= mRegTop) {
                    mMove = false
                    return super.onTouchEvent(event)
                }
                if (mMove) {
                    mMove = false
                    return super.onTouchEvent(event)
                }
            }
        }
        return true
    }

    @RequiresApi(Build.VERSION_CODES.M)
    private fun bottomSheet(day: Int) {
        if (mDialog == null) {
            mDialog = BottomSheetDialog(context).apply {
                setContentView(R.layout.tqv_bottom_sheet)
                val tps = findViewById<TimePicker>(R.id.tps)
                val tpe = findViewById<TimePicker>(R.id.tpe)
                tps?.apply {
                    setIs24HourView(true)
                    hour = mTimeQatm.time?.sh ?: 0
                    minute = mTimeQatm.time?.sm ?: 0
                }
                tpe?.apply {
                    setIs24HourView(true)
                    hour = mTimeQatm.time?.eh ?: 0
                    minute = mTimeQatm.time?.em ?: 0
                }
                val save = findViewById<TextView>(R.id.save)
                save?.setTextColor(bPaint.color)
                save?.setOnClickListener {
                    val sh = tps?.hour ?: 0
                    val sm = tps?.minute ?: 0
                    val eh = tpe?.hour ?: 0
                    val em = tpe?.minute ?: 0
                    val valid = (sh * 60 + sm) <= (eh * 60 + em)
                    if (valid) mockForTouch(day, sh, sm, eh, em)
                    dismiss()
                }
                findViewById<View>(R.id.delete)?.setOnClickListener {
                    tps?.hour = 0
                    tps?.minute = 0
                    tpe?.hour = 0
                    tpe?.minute = 0
                    save?.performClick()
                }
                setOnDismissListener { mTimeQatm = newTQ() }
            }
        }
        mDialog?.show()
    }

    private fun isRegCenter(x: Float, y: Float): Boolean {
        var ret = false
        traversalTQ { _, tq ->
            if (!tq.contains(x, y)) return@traversalTQ false
            val s = tq.top
            val e = tq.bottom
            val t = (e + s) / 2 - getTextHeight(wPaint) * 2f
            val b = (e + s) / 2 + getTextHeight(wPaint) * 1.5f
            ret = y in t..b
            if (ret) mTimeQatm = tq
            return@traversalTQ ret
        }
        return ret
    }

    private fun newTQ() = TimeQuantum(0f, 0f, width.toFloat(), 0f)
    private fun validTQ(tq: TimeQuantum) {
        if (tq.right <= 0) tq.right = width.toFloat()
    }

    private fun traversalTQ(handle: (rect: RectF, tq: TimeQuantum) -> Boolean) {
        for (map in mTQ_map) {
            val day = map.key
            val tqs = map.value
            for (tq in tqs) {
                val rect = mDayState[day]?.rect ?: continue
                if (handle(rect, tq)) return
            }
        }
    }

    private fun removeIfNeeded(tq: TimeQuantum): Boolean {
        tq.apply {
            if (top <= 0 || bottom <= 0 || bottom <= top || time?.offsetMinutes() ?: Int.MAX_VALUE < mMinMinutes) {
                mTQs.remove(this)
                return true
            }
        }
        for (t in mTQs) {
            if (t == tq) continue
            if (!RectF.intersects(t, tq)) continue
            if (t.top < tq.top) tq.top = t.top
            if (t.bottom > tq.bottom) tq.bottom = t.bottom
            mTQs.remove(t)
            break
        }
        return false
    }

    private fun mergeIfNeeded(tq: TimeQuantum): TimeQuantum? {
        if (tq.top <= 0f || tq.bottom <= 0f) return null
        for (t in mTQs) {
            if (t == tq) continue
            if (RectF.intersects(t, tq)) {
                if (tq.top < t.top) tq.bottom = t.bottom else tq.top = t.top
                return t
            }
        }
        return null
    }

    @Suppress("MemberVisibilityCanBePrivate")
    fun getResult(): Map<@Week Int, List<Time>> {
        val map = mutableMapOf<Int, MutableList<Time>>()
        for (mp in mTQ_map) {
            var list = map[mp.key]
            if (list == null) list = mutableListOf()
            for (tq in mp.value) {
                val t = tq.time ?: continue
                list.add(t)
            }
            map[mp.key] = list
        }
        return map
    }

    fun setPeriods(map: Map<@Week Int, List<Time>>) {
        if (map.isNullOrEmpty()) return
        mTQ_map.clear()
        for (wtq in map) {
            for (time in wtq.value) {
                var list = mTQ_map[wtq.key]
                if (list == null) {
                    list = mutableListOf()
                    mTQ_map[wtq.key] = list
                }
                val tq = newTQ().apply {
                    top = getCurY(time.sh, time.sm)
                    bottom = getCurY(time.eh, time.em)
                }
                tq.time = time
                list.add(tq)
            }
        }
        invalidate()
    }

    private fun swtPaint(paint: Paint, @Type type: Int): Paint {
        when (type) {
            TQV_TYPE_TIME_DARK -> {
                tPaint.textSize = tSize
                paint.typeface = Typeface.DEFAULT
                paint.color = resources.getColor(androidx.appcompat.R.color.material_grey_600)
            }
            TQV_TYPE_TIME_LIGHT -> {
                tPaint.textSize = tSize
                paint.typeface = Typeface.DEFAULT_BOLD
                paint.color = resources.getColor(androidx.appcompat.R.color.material_grey_50)
            }
            TQV_TYPE_TIME_MINUTE -> {
                tPaint.textSize = tSize * 1.2f
                paint.typeface = Typeface.DEFAULT_BOLD
                paint.color = bPaint.color
            }
            TQV_TYPE_PERIOD_DARK -> paint.color = resources.getColor(androidx.appcompat.R.color.material_grey_900)
            TQV_TYPE_PERIOD_LIGHT -> paint.color = resources.getColor(androidx.appcompat.R.color.material_grey_850)
            TQV_TYPE_PERIOD_DRAW -> paint.color = lPaint.color and 0x66FFFFFF
            TQV_TYPE_LINE_THIN -> paint.strokeWidth = 1.5f
            TQV_TYPE_LINE_THICK -> paint.strokeWidth = 3f
        }
        return paint
    }

    override fun performClick(): Boolean {
        println("perform click")
        return super.performClick()
    }

    fun setReadOnly(ro: Boolean) {
        mReadOnly = ro
    }

    fun setListener(listener: OnTimeQuantumListener?) {
        mListener = listener
    }

    interface OnTimeQuantumListener {
        fun onReadied()
        fun onChanged(tqs: Map<@Week Int, List<Time>>)
    }

    override fun toString(): String = "TimeQuantumView [version=0.0.1]"
}